/****************************************************************************
 * Copyright (C) 2009-2010 GGA Software Services LLC
 *
 * This file may be distributed and/or modified under the terms of the
 * GNU Affero General Public License version 3 as published by the Free
 * Software Foundation and appearing in the file LICENSE.GPL included in
 * the packaging of this file.
 *
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 ***************************************************************************/

if(!window.chem || !util.Vec2 || !chem.Struct)
	throw new Error("Vec2 and Molecule should be defined first");

if(typeof (String.prototype.trim) === "undefined")
{
	String.prototype.trim = function ()
	{
		return String(this).replace(/^\s+|\s+$/g, '');
	};
}

isUndefined = function (obj)
{
	return util.isUndefined(obj);
}

chem.Molfile = function () {};

chem.Molfile.loadRGroupFragments = true; // TODO: set to load the fragments

chem.Molfile.parseDecimalInt = function (str)
{
	var val = parseInt(str, 10);

	return isNaN(val) ? 0 : val;
};

chem.Molfile.partitionLine = function ( /*string*/ str, /*array of int*/ parts, /*bool*/ withspace)
{
	var res = [];
	for(var i = 0, shift = 0; i < parts.length; ++i)
	{
		res.push(str.slice(shift, shift + parts[i]));
		if(withspace)
			shift++;
		shift += parts[i];
	}
	return res;
};

chem.Molfile.partitionLineFixed = function ( /*string*/ str, /*int*/ itemLength, /*bool*/ withspace)
{
	var res = [];
	for(var shift = 0; shift < str.length; shift += itemLength)
	{
		res.push(str.slice(shift, shift + itemLength));
		if(withspace)
			shift++;
	}
	return res;
};

chem.Molfile.parseCTFile = function (molfileLines)
{
	var ret = null;
	if(molfileLines[0].search("\\$RXN") == 0)
		ret = chem.Molfile.parseRxn(molfileLines);
	else
		ret = chem.Molfile.parseMol(molfileLines);
	ret.initHalfBonds();
	ret.initNeighbors();
	ret.markFragments();
	return ret;
};

chem.Molfile.fmtInfo = {
	bondTypeMap:
	{
		1: chem.Struct.BOND.TYPE.SINGLE,
		2: chem.Struct.BOND.TYPE.DOUBLE,
		3: chem.Struct.BOND.TYPE.TRIPLE,
		4: chem.Struct.BOND.TYPE.AROMATIC,
		5: chem.Struct.BOND.TYPE.SINGLE_OR_DOUBLE,
		6: chem.Struct.BOND.TYPE.SINGLE_OR_AROMATIC,
		7: chem.Struct.BOND.TYPE.DOUBLE_OR_AROMATIC,
		8: chem.Struct.BOND.TYPE.ANY
	},
	bondStereoMap:
	{
		0: chem.Struct.BOND.STEREO.NONE,
		1: chem.Struct.BOND.STEREO.UP,
		4: chem.Struct.BOND.STEREO.EITHER,
		6: chem.Struct.BOND.STEREO.DOWN,
		3: chem.Struct.BOND.STEREO.CIS_TRANS
	},
	v30bondStereoMap:
	{
		0: chem.Struct.BOND.STEREO.NONE,
		1: chem.Struct.BOND.STEREO.UP,
		2: chem.Struct.BOND.STEREO.EITHER,
		3: chem.Struct.BOND.STEREO.DOWN
	},
	bondTopologyMap:
	{
		0: chem.Struct.BOND.TOPOLOGY.EITHER,
		1: chem.Struct.BOND.TOPOLOGY.RING,
		2: chem.Struct.BOND.TOPOLOGY.CHAIN
	},
	countsLinePartition: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6],
	atomLinePartition: [10, 10, 10, 1, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
	bondLinePartition: [3, 3, 3, 3, 3, 3, 3],
	atomListHeaderPartition: [3, 1, 1, 4, 1, 1],
	atomListHeaderLength: 11, // = atomListHeaderPartition.reduce(function(a,b) { return a + b; }, 0)
	atomListHeaderItemLength: 4,
	chargeMap: [0, +3, +2, +1, 0, -1, -2, -3],
	valenceMap: [undefined, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0],
	implicitHydrogenMap: [undefined, 0, 1, 2, 3, 4],
	v30atomPropMap:
	{
		'CHG': 'charge',
		'RAD': 'radical',
		'MASS': 'isotope',
		'VAL': 'explicitValence',
		'HCOUNT': 'hCount',
		'INVRET': 'invRet',
		'SUBST': 'substitutionCount',
		'UNSAT': 'unsaturatedAtom',
		'RBCNT': 'ringBondCount'
	},
	rxnItemsPartition: [3, 3, 3]
};

chem.Molfile.parseAtomLine = function (atomLine)
{
	var mf = chem.Molfile;
	var atomSplit = mf.partitionLine(atomLine, mf.fmtInfo.atomLinePartition);
	var params = {
		// generic
		pp: new util.Vec2(parseFloat(atomSplit[0]), -parseFloat(atomSplit[1])),
		label: atomSplit[4].trim(),
		explicitValence: mf.fmtInfo.valenceMap[mf.parseDecimalInt(atomSplit[10])],

		// obsolete
		massDifference: mf.parseDecimalInt(atomSplit[5]),
		charge: mf.fmtInfo.chargeMap[mf.parseDecimalInt(atomSplit[6])],

		// query
		hCount: mf.parseDecimalInt(mf.parseDecimalInt(atomSplit[8])),
		stereoCare: mf.parseDecimalInt(atomSplit[9]) != 0,

		// reaction
		aam: mf.parseDecimalInt(atomSplit[14]),
		invRet: mf.parseDecimalInt(atomSplit[15]),

		// reaction query
		exactChangeFlag: mf.parseDecimalInt(atomSplit[16]) != 0
	};
	return new chem.Struct.Atom(params);
};

chem.Molfile.stripV30 = function (line)
{
	if(line.slice(0, 7) != 'M  V30 ')
		throw Error("Prefix invalid");
	return line.slice(7);
};

chem.Molfile.parseAtomLineV3000 = function (line)
{
	var mf = chem.Molfile;
	var split, subsplit, key, value, i;
	split = mf.spaceparsplit(line);
	var params = {
		pp: new util.Vec2(parseFloat(split[2]), -parseFloat(split[3])),
		aam: split[5].trim()
	};
	var label = split[1].trim();
	if(label.charAt(0) == "\"" && label.charAt(label.length - 1) == "\"")
	{
		label = label.substr(1, label.length - 2); // strip qutation marks
	}
	if(label.charAt(label.length - 1) == "]")
	{ // assume atom list
		label = label.substr(0, label.length - 1); // remove ']'
		var atomListParams = {};
		atomListParams.notList = false;
		if(label.substr(0, 5) == "NOT [")
		{
			atomListParams.notList = true;
			label = label.substr(5); // remove 'NOT ['
		}
		else if(label.charAt(0) != "[")
		{
			throw new Error("Error: atom list expected, found \'" + label + "\'");
		}
		else
		{
			label = label.substr(1); // remove '['
		}
		atomListParams.ids = mf.labelsListToIds(label.split(","));
		params['atomList'] = new chem.Struct.AtomList(atomListParams);
		params['label'] = 'L#';
	}
	else
	{
		params['label'] = label;
	}
	split.splice(0, 6);
	for(i = 0; i < split.length; ++i)
	{
		subsplit = mf.splitonce(split[i], '=');
		key = subsplit[0];
		value = subsplit[1];
		if(key in mf.fmtInfo.v30atomPropMap)
		{
			var ival = mf.parseDecimalInt(value);
			if(key == 'VAL')
			{
				if(ival == 0)
					continue;
				if(ival == -1)
					ival = 0;
			}
			params[mf.fmtInfo.v30atomPropMap[key]] = ival;
		}
		else if(key == 'RGROUPS')
		{
			value = value.trim().substr(1, value.length - 2);
			var rgrsplit = value.split(' ').slice(1);
			params.rglabel = 0;
			for(var j = 0; j < rgrsplit.length; ++j)
			{
				params.rglabel |= 1 << (rgrsplit[j] - 1);
			}
		}
		else if(key == 'ATTCHPT')
		{
			params.attpnt = value.trim() - 0;
		}
	}
	return new chem.Struct.Atom(params);
};

chem.Molfile.parseBondLineV3000 = function (line)
{
	var mf = chem.Molfile;
	var split, subsplit, key, value, i;
	split = mf.spaceparsplit(line);
	var params = {
		begin: mf.parseDecimalInt(split[2]) - 1,
		end: mf.parseDecimalInt(split[3]) - 1,
		type: mf.fmtInfo.bondTypeMap[mf.parseDecimalInt(split[1])]
	};
	split.splice(0, 4);
	for(i = 0; i < split.length; ++i)
	{
		subsplit = mf.splitonce(split[i], '=');
		key = subsplit[0];
		value = subsplit[1];
		if(key == 'CFG')
		{
			params.stereo = mf.fmtInfo.v30bondStereoMap[mf.parseDecimalInt(value)];
			if(params.type == chem.Struct.BOND.TYPE.DOUBLE && params.stereo == chem.Struct.BOND.STEREO.EITHER)
				params.stereo = chem.Struct.BOND.STEREO.CIS_TRANS;
		}
		else if(key == 'TOPO')
		{
			params.topology = mf.fmtInfo.bondTopologyMap[mf.parseDecimalInt(value)];
		}
		else if(key == 'RXCTR')
		{
			params.reactingCenterStatus = mf.parseDecimalInt(value);
		}
		else if(key == 'STBOX')
		{
			params.stereoCare = mf.parseDecimalInt(value);
		}
	}
	return new chem.Struct.Bond(params);
};

chem.Molfile.parseBondLine = function (bondLine)
{
	var mf = chem.Molfile;
	var bondSplit = mf.partitionLine(bondLine, mf.fmtInfo.bondLinePartition);
	var params = {
		begin: mf.parseDecimalInt(bondSplit[0]) - 1,
		end: mf.parseDecimalInt(bondSplit[1]) - 1,
		type: mf.fmtInfo.bondTypeMap[mf.parseDecimalInt(bondSplit[2])],
		stereo: mf.fmtInfo.bondStereoMap[mf.parseDecimalInt(bondSplit[3])],
		topology: mf.fmtInfo.bondTopologyMap[mf.parseDecimalInt(bondSplit[5])],
		reactingCenterStatus: mf.parseDecimalInt(bondSplit[6])
	};

	return new chem.Struct.Bond(params);
};

chem.Molfile.parseAtomListLine = function ( /* string */ atomListLine)
{
	var mf = chem.Molfile;
	var split = mf.partitionLine(atomListLine, mf.fmtInfo.atomListHeaderPartition);

	var number = mf.parseDecimalInt(split[0]) - 1;
	var notList = (split[2].trim() == "T");
	var count = mf.parseDecimalInt(split[4].trim());

	var ids = atomListLine.slice(mf.fmtInfo.atomListHeaderLength);
	var list = [];
	var itemLength = mf.fmtInfo.atomListHeaderItemLength;
	for(var i = 0; i < count; ++i)
		list[i] = mf.parseDecimalInt(ids.slice(i * itemLength, (i + 1) * itemLength - 1));

	return {
		"aid": number,
		"atomList": new chem.Struct.AtomList(
		{
			"notList": notList,
			"ids": list
		})
	};
};

chem.Molfile.readKeyValuePairs = function (str, /* bool */ valueString)
{
	var mf = chem.Molfile;
	var ret = {};
	var partition = mf.partitionLineFixed(str, 3, true);
	var count = mf.parseDecimalInt(partition[0]);
	for(var i = 0; i < count; ++i)
		ret[mf.parseDecimalInt(partition[2 * i + 1]) - 1] =
		valueString ? partition[2 * i + 2].trim() :
		mf.parseDecimalInt(partition[2 * i + 2]);
	return ret;
};

chem.Molfile.readKeyMultiValuePairs = function (str, /* bool */ valueString)
{
	var mf = chem.Molfile;
	var ret = [];
	var partition = mf.partitionLineFixed(str, 3, true);
	var count = mf.parseDecimalInt(partition[0]);
	for(var i = 0; i < count; ++i)
		ret.push([
            mf.parseDecimalInt(partition[2 * i + 1]) - 1,
            valueString ? partition[2 * i + 2].trim() : mf.parseDecimalInt(partition[2 * i + 2])
        ]);
	return ret;
};

chem.Molfile.labelsListToIds = function (labels)
{
	var ids = [];
	for(var i = 0; i < labels.length; ++i)
	{
		ids.push(chem.Element.getElementByLabel(labels[i].trim()));
	}
	return ids;
};

chem.Molfile.parsePropertyLineAtomList = function (hdr, lst)
{
	var mf = chem.Molfile;
	var aid = mf.parseDecimalInt(hdr[1]) - 1;
	var count = mf.parseDecimalInt(hdr[2]);
	var notList = hdr[4].trim() == 'T';
	var ids = mf.labelsListToIds(lst.slice(0, count));
	var ret = {};
	ret[aid] = new chem.Struct.AtomList(
	{
		"notList": notList,
		"ids": ids
	});
	return ret;
};

chem.Molfile.initSGroup = function (sGroups, propData)
{
	var mf = chem.Molfile;
	var kv = mf.readKeyValuePairs(propData, true);
	for(var key in kv)
	{
		var type = kv[key];
		if(!(type in chem.SGroup.TYPES))
			throw new Error('Unsupported S-group type');
		var sg = new chem.SGroup(type);
		sg.number = key;
		sGroups[key] = sg;
	}
};

chem.Molfile.applySGroupProp = function (sGroups, propName, propData, numeric, core)
{
	var mf = chem.Molfile;
	var kv = mf.readKeyValuePairs(propData, !(numeric));
	for(var key in kv)
	{
		// "core" properties are stored directly in an sgroup, not in sgroup.data
		(core ? sGroups[key] : sGroups[key].data)[propName] = kv[key];
	}
};

chem.Molfile.toIntArray = function (strArray)
{
	var mf = chem.Molfile;
	var ret = [];
	for(var j = 0; j < strArray.length; ++j)
		ret[j] = mf.parseDecimalInt(strArray[j]);
	return ret;
};

chem.Molfile.applySGroupArrayProp = function (sGroups, propName, propData, shift)
{
	var mf = chem.Molfile;
	var sid = mf.parseDecimalInt(propData.slice(1, 4)) - 1;
	var num = mf.parseDecimalInt(propData.slice(4, 8));
	var part = mf.toIntArray(mf.partitionLineFixed(propData.slice(8), 3, true));

	if(part.length != num)
		throw new Error('File format invalid');
	if(shift)
	{
		util.apply(part, function (v)
		{
			return v + shift;
		});
	}
	sGroups[sid][propName] = sGroups[sid][propName].concat(part);
};

chem.Molfile.applyDataSGroupName = function (sg, name)
{
	sg.data.fieldName = name;
};

chem.Molfile.applyDataSGroupQuery = function (sg, query)
{
	sg.data.query = query;
};

chem.Molfile.applyDataSGroupQueryOp = function (sg, queryOp)
{
	sg.data.queryOp = queryOp;
};

chem.Molfile.applyDataSGroupDesc = function (sGroups, propData)
{
	var mf = chem.Molfile;
	var split = mf.partitionLine(propData, [4, 31, 2, 20, 2, 3], false);
	var id = mf.parseDecimalInt(split[0]) - 1;
	var fieldName = split[1].trim();
	var fieldType = split[2].trim();
	var units = split[3].trim();
	var query = split[4].trim();
	var queryOp = split[5].trim();
	var sGroup = sGroups[id];
	sGroup.data.fieldType = fieldType;
	sGroup.data.fieldName = fieldName;
	sGroup.data.units = units;
	sGroup.data.query = query;
	sGroup.data.queryOp = queryOp;
};

chem.Molfile.applyDataSGroupInfo = function (sg, propData)
{
	var mf = chem.Molfile;
	var split = mf.partitionLine(propData, [10 /*x.x*/ , 10 /*y.y*/ , 4 /* eee*/ , 1 /*f*/ , 1 /*g*/ , 1 /*h*/ , 3 /* i */ , 3 /*jjj*/ , 3 /*kkk*/ , 3 /*ll*/ , 2 /*m*/ , 3 /*n*/ , 2 /*oo*/ ], false);

	var x = parseFloat(split[0]);
	var y = parseFloat(split[1]);
	var attached = split[3].trim() == 'A';
	var absolute = split[4].trim() == 'A';
	var showUnits = split[5].trim() == 'U';
	var nCharsToDisplay = split[7].trim();
	nCharsToDisplay = nCharsToDisplay == 'ALL' ? -1 : mf.parseDecimalInt(nCharsToDisplay);
	var tagChar = split[10].trim();
	var daspPos = mf.parseDecimalInt(split[11].trim());

	sg.pp = new util.Vec2(x, -y);
	sg.data.attached = attached;
	sg.data.absolute = absolute;
	sg.data.showUnits = showUnits;
	sg.data.nCharsToDisplay = nCharsToDisplay;
	sg.data.tagChar = tagChar;
	sg.data.daspPos = daspPos;
};

chem.Molfile.applyDataSGroupInfoLine = function (sGroups, propData)
{
	var mf = chem.Molfile;
	var id = mf.parseDecimalInt(propData.substr(0, 4)) - 1;
	var sg = sGroups[id];
	mf.applyDataSGroupInfo(sg, propData.substr(5));
};

chem.Molfile.applyDataSGroupData = function (sg, data, finalize)
{
	sg.data.fieldValue = (sg.data.fieldValue || '') + data;
	if(finalize)
	{
		sg.data.fieldValue = util.stripRight(sg.data.fieldValue);
		if(sg.data.fieldValue.startsWith('"') && sg.data.fieldValue.endsWith('"'))
			sg.data.fieldValue = sg.data.fieldValue.substr(1, sg.data.fieldValue.length - 2);
		sg.data.fieldValue += '\n';
	}
};

chem.Molfile.applyDataSGroupDataLine = function (sGroups, propData, finalize)
{
	var mf = chem.Molfile;
	var id = mf.parseDecimalInt(propData.substr(0, 5)) - 1;
	var data = propData.substr(5);
	var sg = sGroups[id];
	mf.applyDataSGroupData(sg, data, finalize);
};

chem.Molfile.parsePropertyLines = function (ctab, ctabLines, shift, end, sGroups, rLogic)
{
	var mf = chem.Molfile;
	var props = new util.Map();
	while(shift < end)
	{
		var line = ctabLines[shift];
		if(line.charAt(0) == 'A')
		{
			if(!props.get('label'))
				props.set('label', new util.Map());
			props.get('label').set(mf.parseDecimalInt(line.slice(3, 6)) - 1, ctabLines[++shift]);
		}
		else if(line.charAt(0) == 'M')
		{
			var type = line.slice(3, 6);
			var propertyData = line.slice(6);
			if(type == "END")
			{
				break;
			}
			else if(type == "CHG")
			{
				if(!props.get('charge'))
					props.set('charge', new util.Map());
				props.get('charge').update(mf.readKeyValuePairs(propertyData));
			}
			else if(type == "RAD")
			{
				if(!props.get('radical'))
					props.set('radical', new util.Map());
				props.get('radical').update(mf.readKeyValuePairs(propertyData));
			}
			else if(type == "ISO")
			{
				if(!props.get('isotope'))
					props.set('isotope', new util.Map());
				props.get('isotope').update(mf.readKeyValuePairs(propertyData));
			}
			else if(type == "RBC")
			{
				if(!props.get('ringBondCount'))
					props.set('ringBondCount', new util.Map());
				props.get('ringBondCount').update(mf.readKeyValuePairs(propertyData));
			}
			else if(type == "SUB")
			{
				if(!props.get('substitutionCount'))
					props.set('substitutionCount', new util.Map());
				props.get('substitutionCount').update(mf.readKeyValuePairs(propertyData));
			}
			else if(type == "UNS")
			{
				if(!props.get('unsaturatedAtom'))
					props.set('unsaturatedAtom', new util.Map());
				props.get('unsaturatedAtom').update(mf.readKeyValuePairs(propertyData));
				// else if (type == "LIN") // link atom
			}
			else if(type == "RGP")
			{ // rgroup atom
				if(!props.get('rglabel'))
					props.set('rglabel', new util.Map());
				var rglabels = props.get('rglabel');
				var a2rs = mf.readKeyMultiValuePairs(propertyData);
				for(var a2ri = 0; a2ri < a2rs.length; a2ri++)
				{
					var a2r = a2rs[a2ri];
					rglabels.set(a2r[0], (rglabels.get(a2r[0]) || 0) | (1 << (a2r[1] - 1)));
				}
			}
			else if(type == "LOG")
			{ // rgroup atom
				propertyData = propertyData.slice(4);
				var rgid = mf.parseDecimalInt(propertyData.slice(0, 3).trim());
				var iii = mf.parseDecimalInt(propertyData.slice(4, 7).trim());
				var hhh = mf.parseDecimalInt(propertyData.slice(8, 11).trim());
				var ooo = propertyData.slice(12).trim();
				var logic = {};
				if(iii > 0)
					logic.ifthen = iii;
				logic.resth = hhh == 1;
				logic.range = ooo;
				rLogic[rgid] = logic;
			}
			else if(type == "APO")
			{
				if(!props.get('attpnt'))
					props.set('attpnt', new util.Map());
				props.get('attpnt').update(mf.readKeyValuePairs(propertyData));
			}
			else if(type == "ALS")
			{ // atom list
				if(!props.get('atomList'))
					props.set('atomList', new util.Map());
				var list = mf.parsePropertyLineAtomList(
					mf.partitionLine(propertyData, [1, 3, 3, 1, 1, 1]),
					mf.partitionLineFixed(propertyData.slice(10), 4, false));
				props.get('atomList').update(
					list);
				if(!props.get('label'))
					props.set('label', new util.Map());
				for(var aid in list) props.get('label').set(aid, 'L#');
			}
			else if(type == "STY")
			{ // introduce s-group
				mf.initSGroup(sGroups, propertyData);
			}
			else if(type == "SST")
			{
				mf.applySGroupProp(sGroups, 'subtype', propertyData);
			}
			else if(type == "SLB")
			{
				mf.applySGroupProp(sGroups, 'label', propertyData, true);
			}
			else if(type == "SPL")
			{
				mf.applySGroupProp(sGroups, 'parent', propertyData, true, true);
			}
			else if(type == "SCN")
			{
				mf.applySGroupProp(sGroups, 'connectivity', propertyData);
			}
			else if(type == "SAL")
			{
				mf.applySGroupArrayProp(sGroups, 'atoms', propertyData, -1);
			}
			else if(type == "SBL")
			{
				mf.applySGroupArrayProp(sGroups, 'bonds', propertyData, -1);
			}
			else if(type == "SPA")
			{
				mf.applySGroupArrayProp(sGroups, 'patoms', propertyData, -1);
			}
			else if(type == "SMT")
			{
				var sid = mf.parseDecimalInt(propertyData.slice(0, 4)) - 1;
				sGroups[sid].data.subscript = propertyData.slice(4).trim();
			}
			else if(type == "SDT")
			{
				mf.applyDataSGroupDesc(sGroups, propertyData);
			}
			else if(type == "SDD")
			{
				mf.applyDataSGroupInfoLine(sGroups, propertyData);
			}
			else if(type == "SCD")
			{
				mf.applyDataSGroupDataLine(sGroups, propertyData, false);
			}
			else if(type == "SED")
			{
				mf.applyDataSGroupDataLine(sGroups, propertyData, true);
			}
		}
		++shift;
	}
	return props;
};

chem.Molfile.applyAtomProp = function (atoms /* Pool */ , values /* util.Map */ , propId /* string */ , clean /* boolean */ )
{
	values.each(function (aid, propVal)
	{
		atoms.get(aid)[propId] = propVal;
	});
};

chem.Molfile.parseCTabV2000 = function (ctabLines, countsSplit)
{
	var ctab = new chem.Struct();
	var i;
	var mf = chem.Molfile;
	var atomCount = mf.parseDecimalInt(countsSplit[0]);
	var bondCount = mf.parseDecimalInt(countsSplit[1]);
	var atomListCount = mf.parseDecimalInt(countsSplit[2]);
	ctab.isChiral = mf.parseDecimalInt(countsSplit[4]) != 0;
	var stextLinesCount = mf.parseDecimalInt(countsSplit[5]);
	var propertyLinesCount = mf.parseDecimalInt(countsSplit[10]);

	var shift = 0;
	var atomLines = ctabLines.slice(shift, shift + atomCount);
	shift += atomCount;
	var bondLines = ctabLines.slice(shift, shift + bondCount);
	shift += bondCount;
	var atomListLines = ctabLines.slice(shift, shift + atomListCount);
	shift += atomListCount + stextLinesCount;

	var atoms = atomLines.map(mf.parseAtomLine);
	for(i = 0; i < atoms.length; ++i)
		ctab.atoms.add(atoms[i]);
	var bonds = bondLines.map(mf.parseBondLine);
	for(i = 0; i < bonds.length; ++i)
		ctab.bonds.add(bonds[i]);

	var atomLists = atomListLines.map(mf.parseAtomListLine);
	util.each(atomLists, function (pair)
	{
		ctab.atoms.get(pair.aid).atomList = pair.atomList;
		ctab.atoms.get(pair.aid).label = 'L#';
	});

	var sGroups = {},
		rLogic = {};
	var props = mf.parsePropertyLines(ctab, ctabLines, shift,
		Math.min(ctabLines.length, shift + propertyLinesCount), sGroups, rLogic);
	props.each(function (propId, values)
	{
		mf.applyAtomProp(ctab.atoms, values, propId);
	});

	var atomMap = {};
	var sid;
	for(sid in sGroups)
	{
		var sg = sGroups[sid];
		if(sg.type === 'DAT' && sg.atoms.length === 0)
		{
			var parent = sGroups[sid].parent;
			if(parent >= 0)
			{
				var psg = sGroups[parent - 1];
				if(psg.type === 'GEN')
				{
					sg.atoms = util.array(psg.atoms);
				}
			}
		}
	}
	for(sid in sGroups)
	{
		chem.SGroup.addGroup(ctab, sGroups[sid], atomMap);
	}
	var emptyGroups = [];
	for(sid in sGroups)
	{ // TODO: why do we need that?
		chem.SGroup.filter(ctab, sGroups[sid], atomMap);
		if(sGroups[sid].atoms.length == 0 && !sGroups[sid].allAtoms)
			emptyGroups.push(sid);
	}
	for(i = 0; i < emptyGroups.length; ++i)
	{
		ctab.sGroupForest.remove(emptyGroups[i]);
		ctab.sgroups.remove(emptyGroups[i]);
	}
	for(var rgid in rLogic)
	{
		ctab.rgroups.set(rgid, new chem.Struct.RGroup(rLogic[rgid]));
	}
	return ctab;
};

// split a line by spaces outside parentheses
chem.Molfile.spaceparsplit = function (line)
{
	var split = [],
		pc = 0,
		c, i, i0 = -1;
	var line_array = line.toArray(); // IE7 doesn't support line[i]
	var quoted = false;

	for(i = 0; i < line.length; ++i)
	{
		c = line_array[i];
		if(c == '(')
			pc++;
		else if(c == ')')
			pc--;
		if(c == '"')
			quoted = !quoted;
		if(!quoted && line_array[i] == ' ' && pc == 0)
		{
			if(i > i0 + 1)
				split.push(line.slice(i0 + 1, i));
			i0 = i;
		}
	}
	if(i > i0 + 1)
		split.push(line.slice(i0 + 1, i));
	i0 = i;
	return split;
};

chem.Molfile.splitonce = function (line, delim)
{
	var p = line.indexOf(delim);
	return [line.slice(0, p), line.slice(p + 1)];
};

chem.Molfile.splitSGroupDef = function (line)
{
	var split = [];
	var braceBalance = 0;
	var quoted = false;
	for(var i = 0; i < line.length; ++i)
	{
		var c = line.charAt(i);
		if(c == '"')
		{
			quoted = !quoted;
		}
		else if(!quoted)
		{
			if(c == '(')
			{
				braceBalance++;
			}
			else if(c == ')')
			{
				braceBalance--;
			}
			else if(c == ' ' && braceBalance == 0)
			{
				split.push(line.slice(0, i));
				line = line.slice(i + 1).trim();
				i = 0;
			}
		}
	}
	if(braceBalance != 0)
		throw new Error("Brace balance broken. S-group properies invalid!");
	if(line.length > 0)
		split.push(line.trim());
	return split;
};

chem.Molfile.parseBracedNumberList = function (line, shift)
{
	if(!line)
		return null;
	var list = [];
	line = line.trim();
	line = line.substr(1, line.length - 2);
	var split = line.split(" ");
	shift = shift || 0;
	for(var i = 1; i < split.length; ++i)
	{ // skip the first element
		list.push(split[i] - 0 + shift);
	}
	return list;
};

chem.Molfile.v3000parseCollection = function (ctab, ctabLines, shift)
{
	shift++;
	while(ctabLines[shift].trim() != "M  V30 END COLLECTION")
		shift++;
	shift++;
	return shift;
};

chem.Molfile.v3000parseSGroup = function (ctab, ctabLines, sgroups, atomMap, shift)
{
	var mf = chem.Molfile;
	var line = '';
	shift++;
	while(shift < ctabLines.length)
	{
		line = mf.stripV30(ctabLines[shift++]).trim();
		if(line.trim() == 'END SGROUP')
			return shift;
		while(line.charAt(line.length - 1) == '-')
			line = (line.substr(0, line.length - 1) +
				mf.stripV30(ctabLines[shift++])).trim();
		var split = mf.splitSGroupDef(line);
		var type = split[1];
		var sg = new chem.SGroup(type);
		sg.number = split[0] - 0;
		sg.type = type;
		sg.label = split[2] - 0;
		sgroups[sg.number] = sg;
		var props = {};
		for(var i = 3; i < split.length; ++i)
		{
			var subsplit = mf.splitonce(split[i], '=');
			if(subsplit.length != 2)
			{
				throw new Error("A record of form AAA=BBB or AAA=(...) expected, got '" + split[i] + "'");
			}
			var name = subsplit[0];
			if(!(name in props))
				props[name] = [];
			props[name].push(subsplit[1]);
		}
		sg.atoms = mf.parseBracedNumberList(props['ATOMS'][0], -1);
		if(props['PATOMS'])
			sg.patoms = mf.parseBracedNumberList(props['PATOMS'][0], -1);
		sg.bonds = props['BONDS'] ? mf.parseBracedNumberList(props['BONDS'][0], -1) : [];
		var brkxyzStrs = props['BRKXYZ'];
		sg.brkxyz = [];
		if(brkxyzStrs)
		{
			for(var j = 0; j < brkxyzStrs.length; ++j)
				sg.brkxyz.push(mf.parseBracedNumberList(brkxyzStrs[j]));
		}
		if(props['MULT'])
		{
			sg.data.subscript = props['MULT'][0] - 0;
		}
		if(props['LABEL'])
		{
			sg.data.subscript = props['LABEL'][0].trim();
		}
		if(props['CONNECT'])
		{
			sg.data.connectivity = props['CONNECT'][0].toLowerCase();
		}
		if(props['FIELDDISP'])
		{
			mf.applyDataSGroupInfo(sg, util.stripQuotes(props['FIELDDISP'][0]));
		}
		if(props['FIELDDATA'])
		{
			mf.applyDataSGroupData(sg, props['FIELDDATA'][0], true);
		}
		if(props['FIELDNAME'])
		{
			mf.applyDataSGroupName(sg, props['FIELDNAME'][0]);
		}
		if(props['QUERYTYPE'])
		{
			mf.applyDataSGroupQuery(sg, props['QUERYTYPE'][0]);
		}
		if(props['QUERYOP'])
		{
			mf.applyDataSGroupQueryOp(sg, props['QUERYOP'][0]);
		}
		chem.SGroup.addGroup(ctab, sg, atomMap);
	}
	throw new Error("S-group declaration incomplete.");
};

chem.Molfile.parseCTabV3000 = function (ctabLines, norgroups)
{
	var ctab = new chem.Struct();
	var mf = chem.Molfile;

	var shift = 0;
	if(ctabLines[shift++].trim() != "M  V30 BEGIN CTAB")
		throw Error("CTAB V3000 invalid");
	if(ctabLines[shift].slice(0, 13) != "M  V30 COUNTS")
		throw Error("CTAB V3000 invalid");
	var vals = ctabLines[shift].slice(14).split(' ');
	ctab.isChiral = (mf.parseDecimalInt(vals[4]) == 1);
	shift++;

	if(ctabLines[shift].trim() == "M  V30 BEGIN ATOM")
	{
		shift++;
		var line;
		while(shift < ctabLines.length)
		{
			line = mf.stripV30(ctabLines[shift++]).trim();
			if(line == 'END ATOM')
				break;
			while(line.charAt(line.length - 1) == '-')
				line = (line.substring(0, line.length - 1) + mf.stripV30(ctabLines[shift++])).trim();
			ctab.atoms.add(mf.parseAtomLineV3000(line));
		}

		if(ctabLines[shift].trim() == "M  V30 BEGIN BOND")
		{
			shift++;
			while(shift < ctabLines.length)
			{
				line = mf.stripV30(ctabLines[shift++]).trim();
				if(line == 'END BOND')
					break;
				while(line.charAt(line.length - 1) == '-')
					line = (line.substring(0, line.length - 1) + mf.stripV30(ctabLines[shift++])).trim();
				ctab.bonds.add(mf.parseBondLineV3000(line));
			}
		}

		// TODO: let sections follow in arbitrary order
		var sgroups = {};
		var atomMap = {};

		while(ctabLines[shift].trim() != "M  V30 END CTAB")
		{
			if(ctabLines[shift].trim() == "M  V30 BEGIN COLLECTION")
			{
				// TODO: read collection information
				shift = mf.v3000parseCollection(ctab, ctabLines, shift);
			}
			else if(ctabLines[shift].trim() == "M  V30 BEGIN SGROUP")
			{
				shift = mf.v3000parseSGroup(ctab, ctabLines, sgroups, atomMap, shift);
			}
			else
			{
				throw Error("CTAB V3000 invalid");
			}
		}
	}
	if(ctabLines[shift++].trim() != "M  V30 END CTAB")
		throw Error("CTAB V3000 invalid");

	if(!norgroups)
	{
		mf.readRGroups3000(ctab, ctabLines.slice(shift));
	}

	return ctab;
};

chem.Molfile.readRGroups3000 = function (ctab, /* string */ ctabLines) /* chem.Struct */
{
	var rfrags = {};
	var rLogic = {};
	var shift = 0;
	var mf = chem.Molfile;
	while(shift < ctabLines.length && ctabLines[shift].search("M  V30 BEGIN RGROUP") == 0)
	{
		var id = ctabLines[shift++].split(' ').pop();
		rfrags[id] = [];
		rLogic[id] = {};
		while(true)
		{
			var line = ctabLines[shift].trim();
			if(line.search("M  V30 RLOGIC") == 0)
			{
				line = line.slice(13);
				var rlsplit = line.trim().split(/\s+/g);
				var iii = mf.parseDecimalInt(rlsplit[0]);
				var hhh = mf.parseDecimalInt(rlsplit[1]);
				var ooo = rlsplit.slice(2).join(" ");
				var logic = {};
				if(iii > 0)
					logic.ifthen = iii;
				logic.resth = hhh == 1;
				logic.range = ooo;
				rLogic[id] = logic;
				shift++;
				continue;
			}
			if(line != "M  V30 BEGIN CTAB")
				throw Error("CTAB V3000 invalid");
			for(var i = 0; i < ctabLines.length; ++i)
				if(ctabLines[shift + i].trim() == "M  V30 END CTAB")
					break;
			var lines = ctabLines.slice(shift, shift + i + 1);
			var rfrag = this.parseCTabV3000(lines, true);
			rfrags[id].push(rfrag);
			shift = shift + i + 1;
			if(ctabLines[shift].trim() == "M  V30 END RGROUP")
			{
				shift++;
				break;
			}
		}
	}

	for(var rgid in rfrags)
	{
		for(var j = 0; j < rfrags[rgid].length; ++j)
		{
			var rg = rfrags[rgid][j];
			rg.rgroups.set(rgid, new chem.Struct.RGroup(rLogic[rgid]));
			var frid = rg.frags.add(new chem.Struct.Fragment());
			rg.rgroups.get(rgid).frags.add(frid);
			rg.atoms.each(function (aid, atom)
			{
				atom.fragment = frid;
			});
			rg.mergeInto(ctab);
		}
	}
};

chem.Molfile.parseMol = function ( /* string */ ctabLines) /* chem.Struct */
{
	if(ctabLines[0].search("\\$MDL") == 0)
	{
		return this.parseRg2000(ctabLines);
	}
	var struct = this.parseCTab(ctabLines.slice(3));
	struct.name = ctabLines[0].trim();
	return struct;
};

chem.Molfile.parseCTab = function ( /* string */ ctabLines) /* chem.Struct */
{
	var mf = chem.Molfile;
	var countsSplit = mf.partitionLine(ctabLines[0], mf.fmtInfo.countsLinePartition);
	var version = countsSplit[11].trim();
	ctabLines = ctabLines.slice(1);
	if(version == 'V2000')
		return this.parseCTabV2000(ctabLines, countsSplit);
	else if(version == 'V3000')
		return this.parseCTabV3000(ctabLines, !chem.Molfile.loadRGroupFragments);
	else
		throw Error("Molfile version unknown: " + version);
};

chem.MolfileSaver = function (v3000)
{
	this.molecule = null;
	this.molfile = null;

	this.v3000 = v3000 || false
};

chem.MolfileSaver.prototype.prepareSGroups = function (skipErrors)
{
	var mol = this.molecule;
	var sgroups = mol.sgroups;
	var toRemove = [];

	util.each(this.molecule.sGroupForest.getSGroupsBFS().reverse(), function (id)
	{
		var sg = mol.sgroups.get(id);
		try
		{
			sg.prepareForSaving(mol);
		}
		catch(ex)
		{
			if(ui.forwardExceptions)
				throw ex;
			if(skipErrors && typeof (ex.id) == 'number')
			{
				toRemove.push(ex.id);
			}
			else
			{
				throw ex;
			}
		}
	}, this);
	if(toRemove.length > 0)
	{
		throw new Error(toRemove.length.toString() + " invalid S-groups were detected. They will be omitted.");
	}
	for(var i = 0; i < toRemove.length; ++i)
	{
		mol.sGroupDelete(toRemove[i]);
	}
	return mol;
};

chem.MolfileSaver.getComponents = function (molecule)
{
	var ccs = molecule.findConnectedComponents(true);
	var submols = [];
	var barriers = [];
	var arrowPos = null;
	molecule.rxnArrows.each(function (id, item)
	{ // there's just one arrow
		arrowPos = item.pp.x;
	});
	molecule.rxnPluses.each(function (id, item)
	{
		barriers.push(item.pp.x);
	});
	if(arrowPos != null)
		barriers.push(arrowPos);
	barriers.sort(function (a, b)
	{
		return a - b;
	});
	var components = [];

	var i;
	for(i = 0; i < ccs.length; ++i)
	{
		var bb = molecule.getCoordBoundingBox(ccs[i]);
		var c = util.Vec2.lc2(bb.min, 0.5, bb.max, 0.5);
		var j = 0;
		while(c.x > barriers[j])
		++j;
		components[j] = components[j] ||
		{};
		util.Set.mergeIn(components[j], ccs[i]);
	}
	var submolTexts = [];
	var reactants = [],
		products = [];
	for(i = 0; i < components.length; ++i)
	{
		if(!components[i])
		{
			submolTexts.push("");
			continue;
		}
		bb = molecule.getCoordBoundingBox(components[i]);
		c = util.Vec2.lc2(bb.min, 0.5, bb.max, 0.5);
		if(c.x < arrowPos)
			reactants.push(components[i]);
		else
			products.push(components[i]);
	}

	return {
		'reactants': reactants,
		'products': products
	};
};

chem.MolfileSaver.prototype.getCTab = function (molecule, rgroups)
{
	this.molecule = molecule.clone();
	this.molfile = '';
	this.writeCTab2000(rgroups);
	return this.molfile;
};

chem.MolfileSaver.prototype.saveMolecule = function (molecule, skipSGroupErrors, norgroups)
{
	this.reaction = molecule.rxnArrows.count() > 0;
	if(molecule.rxnArrows.count() > 1)
		throw new Error("Reaction may not contain more than one arrow");
	this.molfile = '';
	if(this.reaction)
	{
		if(molecule.rgroups.count() > 0)
			throw new Error("Unable to save the structure - reactions with r-groups are not supported at the moment");
		var components = chem.MolfileSaver.getComponents(molecule);

		var reactants = components.reactants,
			products = components.products,
			all = reactants.concat(products);
		this.molfile = "$RXN\n\n\n\n" + util.paddedInt(reactants.length, 3) + util.paddedInt(products.length, 3) + util.paddedInt(0, 3) + "\n";
		for(var i = 0; i < all.length; ++i)
		{
			var saver = new chem.MolfileSaver(false);
			var submol = molecule.clone(all[i], null, true);
			var molfile = saver.saveMolecule(submol, false, true);
			this.molfile += "$MOL\n" + molfile;
		}
		return this.molfile;
	}

	if(molecule.rgroups.count() > 0)
	{
		if(norgroups)
		{
			molecule = molecule.getScaffold();
		}
		else
		{
			var scaffold = new chem.MolfileSaver(false).getCTab(molecule.getScaffold(), molecule.rgroups);
			this.molfile = "$MDL  REV  1\n$MOL\n$HDR\n\n\n\n$END HDR\n";
			this.molfile += "$CTAB\n" + scaffold + "$END CTAB\n";

			molecule.rgroups.each(function (rgid, rg)
			{
				this.molfile += "$RGP\n";
				this.writePaddedNumber(rgid, 3);
				this.molfile += "\n";
				rg.frags.each(function (fnum, fid)
				{
					var group = new chem.MolfileSaver(false).getCTab(molecule.getFragment(fid));
					this.molfile += "$CTAB\n" + group + "$END CTAB\n";
				}, this);
				this.molfile += "$END RGP\n";
			}, this);
			this.molfile += "$END MOL\n";

			return this.molfile;
		}
	}

	this.molecule = molecule.clone();

	this.prepareSGroups(skipSGroupErrors);

	this.writeHeader();

	// TODO: saving to V3000
	this.writeCTab2000();

	return this.molfile;
};

chem.MolfileSaver.prototype.writeHeader = function ()
{
	var date = new Date();

	this.writeCR(); // TODO: write structure name
	this.writeWhiteSpace(2);
	this.write('MolView');
	this.writeWhiteSpace();
	this.writeCR((date.getMonth() + 1).toPaddedString(2) + date.getDate().toPaddedString(2) + (date.getFullYear() % 100).toPaddedString(2) +
		date.getHours().toPaddedString(2) + date.getMinutes().toPaddedString(2) + '2D 1   1.00000     0.00000     0');
	this.writeCR();
};

chem.MolfileSaver.prototype.write = function (str)
{
	this.molfile += str;
};

chem.MolfileSaver.prototype.writeCR = function (str)
{
	if(arguments.length == 0)
		str = '';

	this.molfile += str + '\n';
};

chem.MolfileSaver.prototype.writeWhiteSpace = function (length)
{
	if(arguments.length == 0) length = 1;
	for(var i = 0; i < length; i++) this.write(' ');
};

chem.MolfileSaver.prototype.writePadded = function (str, width)
{
	this.write(str);
	this.writeWhiteSpace(width - str.length);
};

chem.MolfileSaver.prototype.writePaddedNumber = function (number, width)
{
	var str = (number - 0).toString();

	this.writeWhiteSpace(width - str.length);
	this.write(str);
};

chem.MolfileSaver.prototype.writePaddedFloat = function (number, width, precision)
{
	this.write(util.paddedFloat(number, width, precision));
};

chem.MolfileSaver.prototype.writeCTab2000Header = function ()
{
	this.writePaddedNumber(this.molecule.atoms.count(), 3);
	this.writePaddedNumber(this.molecule.bonds.count(), 3);

	this.writePaddedNumber(0, 3);
	this.writeWhiteSpace(3);
	this.writePaddedNumber(this.molecule.isChiral ? 1 : 0, 3);
	this.writePaddedNumber(0, 3);
	this.writeWhiteSpace(12);
	this.writePaddedNumber(999, 3);
	this.writeCR(' V2000');
};

chem.MolfileSaver.prototype.writeCTab2000 = function (rgroups)
{
	this.writeCTab2000Header();

	this.mapping = {};
	var i = 1;

	var atomList_list = [];
	var atomLabel_list = [];
	this.molecule.atoms.each(function (id, atom)
	{
		this.writePaddedFloat(atom.pp.x, 10, 4);
		this.writePaddedFloat(-atom.pp.y, 10, 4);
		this.writePaddedFloat(0, 10, 4);
		this.writeWhiteSpace();

		var label = atom.label;
		if(atom.atomList != null)
		{
			label = 'L';
			atomList_list.push(id);
		}
		else if(chem.Element.getElementByLabel(label) == null && ['A', 'Q', 'X', '*', 'R#'].indexOf(label) == -1)
		{
			label = 'C';
			atomLabel_list.push(id);
		}
		this.writePadded(label, 3);
		this.writePaddedNumber(0, 2);
		this.writePaddedNumber(0, 3);
		this.writePaddedNumber(0, 3);

		if(isUndefined(atom.hCount))
			atom.hCount = 0;
		this.writePaddedNumber(atom.hCount, 3);

		if(isUndefined(atom.stereoCare))
			atom.stereoCare = 0;
		this.writePaddedNumber(atom.stereoCare, 3);

		this.writePaddedNumber(atom.explicitValence < 0 ? 0 : (atom.explicitValence == 0 ? 15 : atom.explicitValence), 3);

		this.writePaddedNumber(0, 3);
		this.writePaddedNumber(0, 3);
		this.writePaddedNumber(0, 3);

		if(isUndefined(atom.aam))
			atom.aam = 0;
		this.writePaddedNumber(atom.aam, 3);

		if(isUndefined(atom.invRet))
			atom.invRet = 0;
		this.writePaddedNumber(atom.invRet, 3);

		if(isUndefined(atom.exactChangeFlag))
			atom.exactChangeFlag = 0;
		this.writePaddedNumber(atom.exactChangeFlag, 3);

		this.writeCR();

		this.mapping[id] = i;
		i++;
	}, this);

	this.bondMapping = {};
	i = 1;
	this.molecule.bonds.each(function (id, bond)
	{
		this.bondMapping[id] = i++;
		this.writePaddedNumber(this.mapping[bond.begin], 3);
		this.writePaddedNumber(this.mapping[bond.end], 3);
		this.writePaddedNumber(bond.type, 3);

		if(isUndefined(bond.stereo))
			bond.stereo = 0;
		this.writePaddedNumber(bond.stereo, 3);

		this.writeWhiteSpace(3);

		if(isUndefined(bond.topology))
			bond.topology = 0;
		this.writePaddedNumber(bond.topology, 3);

		if(isUndefined(bond.reactingCenterStatus))
			bond.reactingCenterStatus = 0;
		this.writePaddedNumber(bond.reactingCenterStatus, 3);

		this.writeCR();
	}, this);

	while(atomLabel_list.length > 0)
	{
		this.write('A  ');
		this.writePaddedNumber(atomLabel_list[0] + 1, 3);
		this.writeCR();
		this.writeCR(this.molecule.atoms.get(atomLabel_list[0]).label);
		atomLabel_list.splice(0, 1);
	}

	var charge_list = new Array();
	var isotope_list = new Array();
	var radical_list = new Array();
	var rglabel_list = new Array();
	var rglogic_list = new Array();
	var aplabel_list = new Array();
	var rbcount_list = new Array();
	var unsaturated_list = new Array();
	var substcount_list = new Array();

	this.molecule.atoms.each(function (id, atom)
	{
		if(atom.charge != 0)
			charge_list.push([id, atom.charge]);
		if(atom.isotope != 0)
			isotope_list.push([id, atom.isotope]);
		if(atom.radical != 0)
			radical_list.push([id, atom.radical]);
		if(atom.rglabel != null && atom.label == 'R#')
		{ // TODO need to force rglabel=null when label is not 'R#'
			for(var rgi = 0; rgi < 32; rgi++)
			{
				if(atom.rglabel & (1 << rgi)) rglabel_list.push([id, rgi + 1]);
			}
		}
		if(atom.attpnt != null)
			aplabel_list.push([id, atom.attpnt]);
		if(atom.ringBondCount != 0)
			rbcount_list.push([id, atom.ringBondCount]);
		if(atom.substitutionCount != 0)
			substcount_list.push([id, atom.substitutionCount]);
		if(atom.unsaturatedAtom != 0)
			unsaturated_list.push([id, atom.unsaturatedAtom]);
	});

	if(rgroups)
		rgroups.each(function (rgid, rg)
		{
			if(rg.resth || rg.ifthen > 0 || rg.range.length > 0)
			{
				var line = '  1 ' + util.paddedInt(rgid, 3) + ' ' + util.paddedInt(rg.ifthen, 3) + ' ' + util.paddedInt(rg.resth ? 1 : 0, 3) + '   ' + rg.range;
				rglogic_list.push(line);
			}
		});

	var writeAtomPropList = function (prop_id, values)
	{
		while(values.length > 0)
		{
			var part = new Array();

			while(values.length > 0 && part.length < 8)
			{
				part.push(values[0]);
				values.splice(0, 1);
			}

			this.write(prop_id);
			this.writePaddedNumber(part.length, 3);

			util.each(part, function (value)
			{
				this.writeWhiteSpace();
				this.writePaddedNumber(this.mapping[value[0]], 3);
				this.writeWhiteSpace();
				this.writePaddedNumber(value[1], 3);
			}, this);

			this.writeCR();
		}
	};

	writeAtomPropList.call(this, 'M  CHG', charge_list);
	writeAtomPropList.call(this, 'M  ISO', isotope_list);
	writeAtomPropList.call(this, 'M  RAD', radical_list);
	writeAtomPropList.call(this, 'M  RGP', rglabel_list);
	for(var j = 0; j < rglogic_list.length; ++j)
	{
		this.write('M  LOG' + rglogic_list[j] + '\n');
	}
	writeAtomPropList.call(this, 'M  APO', aplabel_list);
	writeAtomPropList.call(this, 'M  RBC', rbcount_list);
	writeAtomPropList.call(this, 'M  SUB', substcount_list);
	writeAtomPropList.call(this, 'M  UNS', unsaturated_list);

	if(atomList_list.length > 0)
	{
		for(j = 0; j < atomList_list.length; ++j)
		{
			var aid = atomList_list[j];
			var atomList = this.molecule.atoms.get(aid).atomList;
			this.write('M  ALS');
			this.writePaddedNumber(aid + 1, 4);
			this.writePaddedNumber(atomList.ids.length, 3);
			this.writeWhiteSpace();
			this.write(atomList.notList ? 'T' : 'F');

			var labelList = atomList.labelList();
			for(var k = 0; k < labelList.length; ++k)
			{
				this.writeWhiteSpace();
				this.writePadded(labelList[k], 3);
			}
			this.writeCR();
		}
	}

	var sgmap = {},
		cnt = 1,
		sgmapback = {};
	var sgorder = this.molecule.sGroupForest.getSGroupsBFS();
	util.each(sgorder, function (id)
	{
		sgmapback[cnt] = id;
		sgmap[id] = cnt++;
	}, this);
	for(var q = 1; q < cnt; ++q)
	{ // each group on its own
		var id = sgmapback[q];
		var sgroup = this.molecule.sgroups.get(id);
		this.write('M  STY');
		this.writePaddedNumber(1, 3);
		this.writeWhiteSpace(1);
		this.writePaddedNumber(q, 3);
		this.writeWhiteSpace(1);
		this.writePadded(sgroup.type, 3);
		this.writeCR();

		// TODO: write subtype, M SST

		this.write('M  SLB');
		this.writePaddedNumber(1, 3);
		this.writeWhiteSpace(1);
		this.writePaddedNumber(q, 3);
		this.writeWhiteSpace(1);
		this.writePaddedNumber(q, 3);
		this.writeCR();

		var parentid = this.molecule.sGroupForest.parent.get(id);
		if(parentid >= 0)
		{
			this.write('M  SPL');
			this.writePaddedNumber(1, 3);
			this.writeWhiteSpace(1);
			this.writePaddedNumber(q, 3);
			this.writeWhiteSpace(1);
			this.writePaddedNumber(sgmap[parentid], 3);
			this.writeCR();
		}

		// connectivity
		if(sgroup.type == 'SRU' && sgroup.data.connectivity)
		{
			var connectivity = '';
			connectivity += ' ';
			connectivity += util.stringPadded(q.toString(), 3);
			connectivity += ' ';
			connectivity += util.stringPadded(sgroup.data.connectivity, 3, true);
			this.write('M  SCN');
			this.writePaddedNumber(1, 3);
			this.write(connectivity.toUpperCase());
			this.writeCR();
		}

		if(sgroup.type == 'SRU')
		{
			this.write('M  SMT ');
			this.writePaddedNumber(q, 3);
			this.writeWhiteSpace();
			this.write(sgroup.data.subscript || 'n');
			this.writeCR();
		}

		this.writeCR(sgroup.saveToMolfile(this.molecule, sgmap, this.mapping, this.bondMapping));
	}

	// TODO: write M  APO
	// TODO: write M  AAL
	// TODO: write M  RGP
	// TODO: write M  LOG

	this.writeCR('M  END');
};

chem.Molfile.parseRxn = function ( /* string[] */ ctabLines) /* chem.Struct */
{
	var mf = chem.Molfile;
	var split = ctabLines[0].trim().split(' ');
	if(split.length > 1 && split[1] == 'V3000')
		return mf.parseRxn3000(ctabLines);
	else
		return mf.parseRxn2000(ctabLines);
};

chem.Molfile.parseRxn2000 = function ( /* string[] */ ctabLines) /* chem.Struct */
{
	var mf = chem.Molfile;
	ctabLines = ctabLines.slice(4);
	var countsSplit = mf.partitionLine(ctabLines[0], mf.fmtInfo.rxnItemsPartition);
	var nReactants = countsSplit[0] - 0,
		nProducts = countsSplit[1] - 0,
		nAgents = countsSplit[2] - 0;
	ctabLines = ctabLines.slice(1); // consume counts line

	var mols = [];
	while(ctabLines.length > 0 && ctabLines[0].substr(0, 4) == "$MOL")
	{
		ctabLines = ctabLines.slice(1);
		var n = 0;
		while(n < ctabLines.length && ctabLines[n].substr(0, 4) != "$MOL") n++;
		mols.push(chem.Molfile.parseMol(ctabLines.slice(0, n)));
		ctabLines = ctabLines.slice(n);
	}
	return mf.rxnMerge(mols, nReactants, nProducts, nAgents);
};

chem.Molfile.parseRxn3000 = function ( /* string[] */ ctabLines) /* chem.Struct */
{
	var mf = chem.Molfile;
	ctabLines = ctabLines.slice(4);
	var countsSplit = ctabLines[0].split(/\s+/g).slice(3);
	var nReactants = countsSplit[0] - 0,
		nProducts = countsSplit[1] - 0,
		nAgents = countsSplit.length > 2 ? countsSplit[2] - 0 : 0;

	var assert = function (condition)
	{
		util.assert(condition, "CTab format invalid");
	};

	var findCtabEnd = function (i)
	{
		for(var j = i; j < ctabLines.length; ++j)
		{
			if(ctabLines[j].trim() == "M  V30 END CTAB")
				return j;
		}
		assert(false);
	};

	var findRGroupEnd = function (i)
	{
		for(var j = i; j < ctabLines.length; ++j)
			if(ctabLines[j].trim() == "M  V30 END RGROUP")
				return j;
		assert(false);
	};

	var molLinesReactants = [],
		molLinesProducts = [],
		current = null,
		rGroups = [];
	for(var i = 0; i < ctabLines.length; ++i)
	{
		var line = ctabLines[i].trim();

		if(line.startsWith("M  V30 COUNTS"))
		{
			// do nothing
		}
		else if(line == "M  END")
		{
			break; // stop reading
		}
		else if(line == "M  V30 BEGIN PRODUCT")
		{
			assert(current == null);
			current = molLinesProducts;
		}
		else if(line == "M  V30 END PRODUCT")
		{
			assert(current === molLinesProducts);
			current = null;
		}
		else if(line == "M  V30 BEGIN REACTANT")
		{
			assert(current == null);
			current = molLinesReactants;
		}
		else if(line == "M  V30 END REACTANT")
		{
			assert(current === molLinesReactants);
			current = null;
		}
		else if(line.startsWith("M  V30 BEGIN RGROUP"))
		{
			assert(current == null);
			var j = findRGroupEnd(i);
			rGroups.push(ctabLines.slice(i, j + 1));
			i = j;
		}
		else if(line == "M  V30 BEGIN CTAB")
		{
			var j = findCtabEnd(i);
			current.push(ctabLines.slice(i, j + 1));
			i = j;
		}
		else
		{
			throw new Error("Line unrecognized: " + line);
		}
	}
	var mols = [];
	var molLines = molLinesReactants.concat(molLinesProducts);
	for(var j = 0; j < molLines.length; ++j)
	{
		var mol = chem.Molfile.parseCTabV3000(molLines[j], countsSplit);
		mols.push(mol);
	}
	var ctab = mf.rxnMerge(mols, nReactants, nProducts, nAgents);

	mf.readRGroups3000(ctab, function (array)
	{
		var res = [];
		for(var k = 0; k < array.length; ++k)
		{
			res = res.concat(array[k]);
		}
		return res;
	}(rGroups));

	return ctab;
};

chem.Molfile.rxnMerge = function (mols, nReactants, nProducts, nAgents) /* chem.Struct */
{
	var mf = chem.Molfile;

	var ret = new chem.Struct();
	var bbReact = [],
		bbAgent = [],
		bbProd = [];
	var molReact = [],
		molAgent = [],
		molProd = [];
	var j;
	var bondLengthData = {
		cnt: 0,
		totalLength: 0
	};
	for(j = 0; j < mols.length; ++j)
	{
		var mol = mols[j];
		var bondLengthDataMol = mol.getBondLengthData();
		bondLengthData.cnt += bondLengthDataMol.cnt;
		bondLengthData.totalLength += bondLengthDataMol.totalLength;
	}
	var avgBondLength = 1 / (bondLengthData.cnt == 0 ? 1 : bondLengthData.totalLength / bondLengthData.cnt);
	for(j = 0; j < mols.length; ++j)
	{
		mol = mols[j];
		mol.scale(avgBondLength);
	}

	for(j = 0; j < mols.length; ++j)
	{
		mol = mols[j];
		var bb = mol.getCoordBoundingBoxObj();
		if(!bb)
			continue;

		var fragmentType = (j < nReactants ? chem.Struct.FRAGMENT.REACTANT :
			(j < nReactants + nProducts ? chem.Struct.FRAGMENT.PRODUCT :
				chem.Struct.FRAGMENT.AGENT));
		if(fragmentType == chem.Struct.FRAGMENT.REACTANT)
		{
			bbReact.push(bb);
			molReact.push(mol);
		}
		else if(fragmentType == chem.Struct.FRAGMENT.AGENT)
		{
			bbAgent.push(bb);
			molAgent.push(mol);
		}
		else if(fragmentType == chem.Struct.FRAGMENT.PRODUCT)
		{
			bbProd.push(bb);
			molProd.push(mol);
		}

		mol.atoms.each(function (aid, atom)
		{
			atom.rxnFragmentType = fragmentType;
		});
	}

	// reaction fragment layout
	var xorig = 0;
	var shiftMol = function (ret, mol, bb, xorig, over)
	{
		var d = new util.Vec2(xorig - bb.min.x, over ? 1 - bb.min.y : -(bb.min.y + bb.max.y) / 2);
		mol.atoms.each(function (aid, atom)
		{
			atom.pp.add_(d);
		});
		mol.sgroups.each(function (id, item)
		{
			if(item.pp)
				item.pp.add_(d);
		});
		bb.min.add_(d);
		bb.max.add_(d);
		mol.mergeInto(ret);
		return bb.max.x - bb.min.x;
	};

	for(j = 0; j < molReact.length; ++j)
	{
		xorig += shiftMol(ret, molReact[j], bbReact[j], xorig, false) + 2.0;
	}
	xorig += 2.0;
	for(j = 0; j < molAgent.length; ++j)
	{
		xorig += shiftMol(ret, molAgent[j], bbAgent[j], xorig, true) + 2.0;
	}
	xorig += 2.0;

	for(j = 0; j < molProd.length; ++j)
	{
		xorig += shiftMol(ret, molProd[j], bbProd[j], xorig, false) + 2.0;
	}

	var bb1, bb2, x, y, bbReactAll = null,
		bbProdAll = null;
	for(j = 0; j < bbReact.length - 1; ++j)
	{
		bb1 = bbReact[j];
		bb2 = bbReact[j + 1];

		x = (bb1.max.x + bb2.min.x) / 2;
		y = (bb1.max.y + bb1.min.y + bb2.max.y + bb2.min.y) / 4;

		ret.rxnPluses.add(new chem.Struct.RxnPlus(
		{
			'pp': new util.Vec2(x, y)
		}));
	}
	for(j = 0; j < bbReact.length; ++j)
	{
		if(j == 0)
		{
			bbReactAll = {};
			bbReactAll.max = new util.Vec2(bbReact[j].max);
			bbReactAll.min = new util.Vec2(bbReact[j].min);
		}
		else
		{
			bbReactAll.max = util.Vec2.max(bbReactAll.max, bbReact[j].max);
			bbReactAll.min = util.Vec2.min(bbReactAll.min, bbReact[j].min);
		}
	}
	for(j = 0; j < bbProd.length - 1; ++j)
	{
		bb1 = bbProd[j];
		bb2 = bbProd[j + 1];

		x = (bb1.max.x + bb2.min.x) / 2;
		y = (bb1.max.y + bb1.min.y + bb2.max.y + bb2.min.y) / 4;

		ret.rxnPluses.add(new chem.Struct.RxnPlus(
		{
			'pp': new util.Vec2(x, y)
		}));
	}
	for(j = 0; j < bbProd.length; ++j)
	{
		if(j == 0)
		{
			bbProdAll = {};
			bbProdAll.max = new util.Vec2(bbProd[j].max);
			bbProdAll.min = new util.Vec2(bbProd[j].min);
		}
		else
		{
			bbProdAll.max = util.Vec2.max(bbProdAll.max, bbProd[j].max);
			bbProdAll.min = util.Vec2.min(bbProdAll.min, bbProd[j].min);
		}
	}
	bb1 = bbReactAll;
	bb2 = bbProdAll;
	if(!bb1 && !bb2)
	{
		ret.rxnArrows.add(new chem.Struct.RxnArrow(
		{
			'pp': new util.Vec2(0, 0)
		}));
	}
	else
	{
		var v1 = bb1 ? new util.Vec2(bb1.max.x, (bb1.max.y + bb1.min.y) / 2) : null;
		var v2 = bb2 ? new util.Vec2(bb2.min.x, (bb2.max.y + bb2.min.y) / 2) : null;
		var defaultOffset = 3;
		if(!v1)
			v1 = new util.Vec2(v2.x - defaultOffset, v2.y);
		if(!v2)
			v2 = new util.Vec2(v1.x + defaultOffset, v1.y);
		ret.rxnArrows.add(new chem.Struct.RxnArrow(
		{
			'pp': util.Vec2.lc2(v1, 0.5, v2, 0.5)
		}));
	}
	ret.isReaction = true;
	return ret;
};

chem.Molfile.rgMerge = function (scaffold, rgroups) /* chem.Struct */
{
	var ret = new chem.Struct();

	scaffold.mergeInto(ret, null, null, false, true);
	for(var rgid in rgroups)
	{
		for(var j = 0; j < rgroups[rgid].length; ++j)
		{
			var ctab = rgroups[rgid][j];
			ctab.rgroups.set(rgid, new chem.Struct.RGroup());
			var frid = ctab.frags.add(new chem.Struct.Fragment());
			ctab.rgroups.get(rgid).frags.add(frid);
			ctab.atoms.each(function (aid, atom)
			{
				atom.fragment = frid;
			});
			ctab.mergeInto(ret);
		}
	}

	return ret;
};

chem.Molfile.parseRg2000 = function ( /* string[] */ ctabLines) /* chem.Struct */
{
	var mf = chem.Molfile;
	ctabLines = ctabLines.slice(7);
	if(ctabLines[0].trim() != '$CTAB')
		throw new Error('RGFile format invalid');
	var i = 1;
	while(ctabLines[i].charAt(0) != '$') i++;
	if(ctabLines[i].trim() != '$END CTAB')
		throw new Error('RGFile format invalid');
	var coreLines = ctabLines.slice(1, i);
	ctabLines = ctabLines.slice(i + 1);
	var fragmentLines = {};
	while(true)
	{
		if(ctabLines.length == 0)
			throw new Error('Unexpected end of file');
		var line = ctabLines[0].trim();
		if(line == '$END MOL')
		{
			ctabLines = ctabLines.slice(1);
			break;
		}
		if(line != '$RGP')
			throw new Error('RGFile format invalid');
		var rgid = ctabLines[1].trim() - 0;
		fragmentLines[rgid] = [];
		ctabLines = ctabLines.slice(2);
		while(true)
		{
			if(ctabLines.length == 0)
				throw new Error('Unexpected end of file');
			line = ctabLines[0].trim();
			if(line == '$END RGP')
			{
				ctabLines = ctabLines.slice(1);
				break;
			}
			if(line != '$CTAB')
				throw new Error('RGFile format invalid');
			i = 1;
			while(ctabLines[i].charAt(0) != '$') i++;
			if(ctabLines[i].trim() != '$END CTAB')
				throw new Error('RGFile format invalid');
			fragmentLines[rgid].push(ctabLines.slice(1, i));
			ctabLines = ctabLines.slice(i + 1);
		}
	}

	var core = chem.Molfile.parseCTab(coreLines),
		frag = {};
	if(chem.Molfile.loadRGroupFragments)
	{
		for(var id in fragmentLines)
		{
			frag[id] = [];
			for(var j = 0; j < fragmentLines[id].length; ++j)
			{
				frag[id].push(chem.Molfile.parseCTab(fragmentLines[id][j]));
			}
		}
	}
	return mf.rgMerge(core, frag);
};
